Skip to content

Refactor InfrahubNode to avoid dynamic class creation #412

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jun 4, 2025

Conversation

ogenstad
Copy link
Contributor

@ogenstad ogenstad commented May 17, 2025

The main goal of this PR is to remove two lines:

self.__class__ = type(f"{schema.kind}InfrahubNode", (self.__class__,), {})

and:

self.__class__ = type(f"{schema.kind}InfrahubNodeSync", (self.__class__,), {})

What these lines do are to create a dynamic Python class for each object we define with the SDK when we create an InfrahubNode or InfrahubNodeSync object.

The main need for the creation of a new object came from the generate_relationship_property() function that created a setter/getter for cardinality=one relationships on the new dynamically created class.

This allowed you to override the data directly on a relationship of cardinality one at the attribute level. In pseudo code if we had a node that looked like this:

class InfraInterface:
    name: Attribute
    device: Relationship

Then if we had an instance of this as an InfrahubNode object:

interface.name.value = "Ethernet1" # Correctly sets the name attribute
interface.name = "Ethernet1" # Previously this would overwrite the name attribute and instead set "name" as a simple `str`, which wouldn't work
interface.device = "453a7da5-2c71-4cac-b96f-e00b787e0436" # this uses the attribute setter and assigns a value under the relationship

With the new implementation both attributes and cardinality=one relationships have both a setter and a getter, so the example above with interface.name = "Ethernet1". Using the same type of logic we'd be able to add a setter to cardinality=many relationships as well, we'd only have to decide how it would work if it should add a relationship or replace all previous ones.

This PR changes the way we would have to manage typing, there are a few variations we could do for this an example can be seen in the two current commits of this PR within the file query_groups.py on the line self.previous_members = group.members.peers the first commit only updates the rules, the second commit removes the ignore rules but could make the code a bit harder to read.

Test pipeline within the Infrahub project: opsmill/infrahub#6492

One change with this is that when using a Python REPL and using dir(my_infrahub_node_object) the name of the attributes and relationships won't be seen as attribute objects.

I'm also thinking if we should have infrahubnode_ as a reserved prefix so that we can add public functions using that name without having to fear future conflicts.

Copy link

cloudflare-workers-and-pages bot commented May 17, 2025

Deploying infrahub-sdk-python with  Cloudflare Pages  Cloudflare Pages

Latest commit: 55d3d84
Status: ✅  Deploy successful!
Preview URL: https://87f88962.infrahub-sdk-python.pages.dev
Branch Preview URL: https://pog-avoid-dynamic-class-crea.infrahub-sdk-python.pages.dev

View logs

Copy link

codecov bot commented May 17, 2025

Codecov Report

Attention: Patch coverage is 68.13187% with 29 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
infrahub_sdk/node/node.py 72.28% 15 Missing and 8 partials ⚠️
infrahub_sdk/ctl/generator.py 0.00% 4 Missing ⚠️
infrahub_sdk/query_groups.py 50.00% 2 Missing ⚠️
@@             Coverage Diff             @@
##           develop     #412      +/-   ##
===========================================
- Coverage    75.38%   75.26%   -0.12%     
===========================================
  Files          100      100              
  Lines         8705     8758      +53     
  Branches      1691     1706      +15     
===========================================
+ Hits          6562     6592      +30     
- Misses        1678     1693      +15     
- Partials       465      473       +8     
Flag Coverage Δ
integration-tests 34.51% <20.87%> (-0.20%) ⬇️
python-3.10 47.60% <51.64%> (-0.04%) ⬇️
python-3.11 47.57% <51.64%> (-0.06%) ⬇️
python-3.12 47.57% <51.64%> (-0.02%) ⬇️
python-3.13 47.55% <51.64%> (-0.04%) ⬇️
python-3.9 45.91% <51.64%> (-0.05%) ⬇️
python-filler-3.12 24.49% <17.58%> (+<0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
infrahub_sdk/query_groups.py 60.60% <50.00%> (+0.30%) ⬆️
infrahub_sdk/ctl/generator.py 51.78% <0.00%> (ø)
infrahub_sdk/node/node.py 75.13% <72.28%> (-1.44%) ⬇️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@@ -94,7 +94,7 @@ async def get_group(self, store_peers: bool = False) -> InfrahubNode | None:
if not store_peers:
return group

self.previous_members = group.members.peers # type: ignore[attr-defined]
self.previous_members = group._relationship_cardinality_many_data["members"].peers
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this one feels wrong to me, I think we should either use the Protocol or we should add a public method to access an attribute or a relationship by its name

group.get_relationship_many(name="members").peers

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes I agree, this was mainly to highlight the typing issue. I'm a bit reluctant to add new public methods, at least until we create some reservations for what we use. I like the way pydantic has done this with model_. Still even the code above could be a bit cleaner with a private method, alternatively we could use the option from the earlier commit where this was left as group.members.peers but with another mypy ignore code.

@ogenstad ogenstad force-pushed the pog-avoid-dynamic-class-creation branch from cfd51de to 0840079 Compare June 2, 2025 09:07
@ogenstad
Copy link
Contributor Author

ogenstad commented Jun 2, 2025

This is ready for review now. I've added some private methods to get attributes or relationships as well as the __dir__() methods along with some changes for the typing as a result of this change.

@ogenstad ogenstad requested a review from a team June 2, 2025 10:58
@ogenstad ogenstad force-pushed the pog-avoid-dynamic-class-creation branch from 0840079 to 55d3d84 Compare June 4, 2025 05:38
@ogenstad
Copy link
Contributor Author

ogenstad commented Jun 4, 2025

Rebased and squashed.

@ogenstad ogenstad merged commit dd16f8d into develop Jun 4, 2025
19 checks passed
@ogenstad ogenstad deleted the pog-avoid-dynamic-class-creation branch June 4, 2025 05:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants